using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Threading;
using BGM.Domain.DTO;


namespace BGM.Sensors.MeasuringDevice
{
    public class PrecisionXceedDeviceDriver : IDisposable, IMeasuringDevice
    {
        #region PortSettings
        private const int BAUD_RATE = 9600,
                          DATA_BITS = 8;
        private const StopBits STOP_BITS = StopBits.One;
        private const Parity PARITY = Parity.None;
        #endregion

        #region Fields
        public event EventHandler ConnectionError;
        private Watchdog watchDog;
        private IPort port;
        private ManualResetEvent waitForDevice = new ManualResetEvent(false);
        private ManualResetEvent dataRecieved = new ManualResetEvent(false);
        private ControlChar lastReadControlChar = ControlChar.NOT_DEFINED;
        private string message = string.Empty;
        private bool messageIsComming = false;
        private List<ControlChar> recievedControlCharList = new List<ControlChar>();
        private string[] messageTypes = new string[] { "1ID", "1GET_METER", "1GET_EVENTS" };
        #endregion
        /// <summary>
        /// Known supported measurement types
        /// </summary>
        public enum ReadingType
        {
            BloodGlucoseReading = 1,
            Unknown2 = 2,
            Unknown3 = 3,
            KetoneReading = 4,
            Unknown5 = 5,
            Unknown6 = 6,
            Unknown7 = 7,
            Unknown8 = 8,
            Unknown9 = 9,
            CurrentBloodGlucoseTestStripLotCode = 10,
            Unknown11 = 11,
            CurrentKetoneTestStripCode = 12,
            NotSet
        }
        /// <summary>
        /// Control chars used in messages with device
        /// </summary>
        private enum ControlChar
        {
            STX = 0x02,
            ETX = 0x03,
            EOT = 0x04,
            ENQ = 0x05,
            ACK = 0x06,
            TAB = 0x09,
            CR = 0x0d,
            LF = 0x0a,
            NAK = 0x15,
            ETB = 0x17,
            NOT_DEFINED = 0
        }
        /// <summary>
        /// Message types used on device
        /// </summary>
        public enum MessageType
        {
            ID = 0,
            GET_METER = 1,
            GET_EVENTS = 2
        }

        /// <summary>
        /// Event raised when a message is recieved
        /// </summary>
        public event MessageRecievedEventhandler MessageRecieved;

        /// <summary>
        /// Constructor
        /// </summary>
        /// <param name="portName">A valid port to communicate with device</param>
        public PrecisionXceedDeviceDriver()//string portName)
        {
            //this.port = new SerialPort(portName, BAUD_RATE, PARITY, DATA_BITS, STOP_BITS);
            //this.port.Handshake = Handshake.XOnXOff;
            //this.port.DtrEnable = true;
            //this.port.RtsEnable = true;
            //this.port.DataReceived += new SerialDataReceivedEventHandler(port_DataReceived);
            lastReadControlChar = ControlChar.NOT_DEFINED;
            watchDog = new Watchdog(new TimeSpan(0, 0, 10), this.RaiseTimeOutException);
        }

        private Collection<string> Open()
        {
            watchDog.Start();
            if (!port.IsOpen)
                port.Open();
            else
            {
                port.Close();
                port.Open();
            }
            SendControlChar(ControlChar.ENQ);
            return RecieveMessages();
        }

        #region Public methods
        /// <summary>
        /// Establish connection with device
        /// </summary>
        /// <returns></returns>
        public Collection<string> Open(string serialPortName)
        {
            CustomSerialPort p = new CustomSerialPort(serialPortName, BAUD_RATE, PARITY, DATA_BITS, STOP_BITS);
            p.Handshake = Handshake.XOnXOff;
            p.DtrEnable = true;
            p.RtsEnable = true;
            p.DataReceived += new EventHandler(port_DataReceived);
            this.port = p;
          return Open();
        }

        public Collection<string> Open(Stream stream)
        {
            IPort p = new StreamPort(stream);
            p.DataReceived += new EventHandler(port_DataReceived);
            this.port = p;
            return Open();
        }
      
        /// <summary>
        /// Recieve messages from device.
        /// This can only be used if a message has been requested from the device
        /// </summary>
        /// <returns></returns>
        public Collection<string> RecieveMessages()
        {
            watchDog.Start();
            Collection<string> messages = new Collection<string>();
            waitForDevice.WaitOne();
            waitForDevice.Reset();
            if (lastReadControlChar == ControlChar.ENQ)
            {
                messageIsComming = true;
                SendControlChar(ControlChar.ACK);
                watchDog.Start();
                while (lastReadControlChar != ControlChar.EOT)
                {
                    if (dataRecieved.WaitOne(200, true))
                    {

                        dataRecieved.Reset();
                        messageIsComming = false;
                        messages.Add(message);
                        OnMessageRecieved(new MessageRecievedEventArgs(messages.Count, message));
                        message = string.Empty;
                        waitForDevice.Reset();
                        messageIsComming = true;
                        watchDog.Start();
                        SendControlChar(ControlChar.ACK);

                    }
                }
                messageIsComming = false;
                message = string.Empty;
                waitForDevice.WaitOne();
                waitForDevice.Reset();
            }
            watchDog.Signal();
            //else
            //{
            //    Thread.Sleep(2000);
            //    if (!port.IsOpen)
            //        port.Open();
            //    else
            //    {
            //        port.Close();
            //        port.Open();
            //    }
            //    SendControlChar(ControlChar.ENQ);
            //    RecieveMessage5();
            //}
            return messages;
        }
        /// <summary>
        /// Send a message request to the device
        /// </summary>
        /// <param name="messageType">Message request type</param>
        public void SendMessage(MessageType messageType)
        {
            string messageToSend = messageTypes[(int)messageType];
            SendControlChar(ControlChar.ENQ);
            waitForDevice.WaitOne();
            waitForDevice.Reset();
            if (lastReadControlChar == ControlChar.ACK)
            {
                port.Write(EncodeMessage(messageToSend));
                waitForDevice.WaitOne();
                waitForDevice.Reset();
                if (lastReadControlChar == ControlChar.ACK)
                {
                    SendControlChar(ControlChar.EOT);
                }
            }
        }

        /// <summary>
        /// Encode message. Add Control chars, start, check and end.
        /// </summary>
        /// <param name="message">Message text</param>
        /// <returns>Encodes message to be used on device</returns>
        public string EncodeMessage(string message)
        {
            message = message.Trim();
            string tmp = string.Empty;// ((char)0x02).ToString();
            tmp += message;
            tmp += CC2S(ControlChar.ETX);
            uint t = 0;
            foreach (char var in tmp)
            {
                t += (uint)var;
            }
            byte b = (byte)(t & 0x00FF); //remoce most significant byte
            uint d = t & 0xFF00; //remoce most significant byte
            string tt = CC2S(ControlChar.STX) + message + ((char)0x03).ToString() + b.ToString("X") + CC2S(ControlChar.CR) + CC2S(ControlChar.LF);
            return tt;
        }
        /// <summary>
        /// Decode message. returns DTO or string if decode failds
        /// </summary>
        /// <param name="message">Message to decode. Check sum will be checked as well</param>
        /// <returns>Decoded message as DTO or string on faild</returns>
        public object DecodeMessage(string message)
        {
            message = message.Trim();
            string tmp = string.Empty;
            if (message.Length > 1)
                tmp = message.Remove(0, 1);
            string data = string.Empty;
            uint t = 0;
            foreach (char var in tmp)
            {
                t += (uint)var;

                if (var == (char)ControlChar.ETX || var == (char)ControlChar.ETB)
                    break;
                data += var;
            }
            byte b = (byte)(t & 0x00FF); //remoce most significant byte
            uint d = t & 0xFF00; //remoce most significant byte
            if (message.Length >= data.Length + 1 + 2 && message.Substring(message.LastIndexOf(data) + data.Length + 1, 2) == b.ToString("X"))
            {
                string d1 = string.Empty;
                if (data.Split(new char[] { ((char)ControlChar.TAB) }).Length > 4)
                {
                    return ParseMessageToDTO(data);
                }
                return data;
            }

            return data;


        }
        #endregion


        //public List<Reading> GetReadings(string serialPortName)
        //{
        //    Open(serialPortName);
        //    return GetReadings();
        //}
        //public List<Reading> GetReadings(Stream stream)
        //{
        //    Open(stream);
        //    return GetReadings();
        //}
        public List<Reading> GetReadings(object obj)
        {
            if (obj is string)
                Open(obj as string);
            else if (obj is Stream)
                Open(obj as Stream);
            else
                return null;

            SendMessage(MessageType.GET_EVENTS);
            List<Reading> readings = new List<Reading>();
            foreach (string message in RecieveMessages())
            {
                object o = DecodeMessage(message);
                if (o is Reading)
                    readings.Add(o as Reading);
            }
            Close();
            readings.Sort();
            return readings;
        }

        public void Close()
        {
            try
            {
                if (port.IsOpen)
                    port.Close();
            }
            catch (InvalidOperationException)
            {


            }
            this.lastReadControlChar = ControlChar.NOT_DEFINED;
            this.watchDog = new Watchdog(new TimeSpan(0, 0, 10), this.RaiseTimeOutException);
            this.waitForDevice = new ManualResetEvent(false);
            this.dataRecieved = new ManualResetEvent(false);
            this.message = string.Empty;
            this.messageIsComming = false;
            this.recievedControlCharList = new List<ControlChar>();
        }

        #region Help methods
        /// <summary>
        /// Parse message to DTO object
        /// </summary>
        /// <param name="message">Message</param>
        /// <returns>DTO or string if convertion failds</returns>
        object ParseMessageToDTO(string message)
        {
            try
            {
                string[] data = message.Split(new char[] { ((char)ControlChar.TAB) });
                ReadingType type = ReadingType.NotSet;
                int seqvenceNumber = -1;
                if (data.Length >= 6)
                {
                    var timeStamp = DateTime.ParseExact(data[2] + " " + data[3], "yyyyMMdd HH:mm", new CultureInfo(""));
                    //timeStamp = DateTime.Parse( new DateTime.();
                    if (data[0].Length >= 2)
                    {
                        seqvenceNumber = int.Parse(data[0].Substring(0, 1));
                        int tmpType = int.Parse(data[0].Remove(0, 1));
                        type = (ReadingType)Enum.Parse(typeof(ReadingType), Enum.GetName(typeof(ReadingType), tmpType));
                    }
                    var flag = int.Parse(data[1]);
                    var value = int.Parse(data[4]);
                    var unknown = int.Parse(data[5].Replace('\r', ' '));


                    if (type == ReadingType.BloodGlucoseReading)
                        return new BloodGlucoseReading(timeStamp, flag, value, unknown, seqvenceNumber);
                    else
                        return message;
                }
            }
            catch (Exception)
            {


            }



            return message;
        }
        /// <summary>
        /// Convert Control enum to string
        /// </summary>
        /// <param name="c">ControlChar enum</param>
        /// <returns>Control char as a string</returns>
        private string CC2S(ControlChar c)
        {
            return ((char)c).ToString();
        }
        /// <summary>
        /// Handles data recieved from device.
        /// Sets flags dataRecieved & waitForDevice
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void port_DataReceived(object sender, EventArgs e)
        {
            lock (this)
            {

                string data = port.ReadExisting();
                //Debug.WriteLine("Data read from device: " + data);
                if (messageIsComming)
                {
                    message += data;
                    //  Debug.WriteLine("Recieved: " + message);
                    // messageIsComming = false;
                    if (message.Length > 4 && message[message.Length - 1] == (char)ControlChar.LF && message[message.Length - 2] == (char)ControlChar.CR &&
                        (message[message.Length - 5] == (char)ControlChar.ETX || message[message.Length - 5] == (char)ControlChar.ETB))
                        dataRecieved.Set();
                }

                foreach (char var in data)
                {
                    foreach (int c in Enum.GetValues(typeof(ControlChar)))
                        if (c == var)
                        {
                            watchDog.Signal();
                            lastReadControlChar = (ControlChar)var;
                            recievedControlCharList.Add((ControlChar)var);
                            //Debug.WriteLine("Data read from deivce parsed as: " + lastReadControlChar.ToString());
                            waitForDevice.Set();
                        }
                }
            }

        }
        /// <summary>
        /// Send control char to device
        /// </summary>
        /// <param name="c">Enum controlchar to send</param>
        private void SendControlChar(ControlChar c)
        {
            //Debug.WriteLine("Sending control Char: " + c.ToString());
            port.Write(((char)c).ToString());
        }
        protected void OnMessageRecieved(MessageRecievedEventArgs e)
        {
            MessageRecievedEventhandler handler = MessageRecieved;
            if (handler != null)
                handler(this, e);
        }
        private void RaiseTimeOutException()
        {
            EventHandler handler = ConnectionError;
            if (handler != null)
                handler(this, new EventArgs());
        }

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Close();
        }

        #endregion



        #region IPlugin Members

        public string Name
        {
            get { return "Precision Xceed Driver"; }
        }

        public string Description
        {
            get { return "Supports uploading of measurements (glucose & ketone)"; }
        }

        #endregion
    }
}
